module dungeon.map;

import derelict.opengl.gl;

import nade.vector;
import nade.region;
import nade.color;
import nade.grid;

import dungeon.game;
import dungeon.map_object;
import dungeon.tileset;


interface IIterator {
	void iterate();
}

interface IEffect {
	void render();
}


class Map {
	struct Tile {
		TileType			type;
		bool[MapObject]		occupants;
	}


	this(Game game, uint width, uint height, int depth, string fill = "", Tileset tileset = null)
	{
		_game = game;
		_depth = depth;
		_tileset = (tileset is null) ? game.getRandomTileset : tileset;
		_fill = _tileset.getTile(fill);
		_grid = new DynamicGrid!(Tile)(width, height, Tile(_fill));
	}

	void render(const Region2 area)
	{
		bool[MapObject][uint] objs;

		glEnable(GL_TEXTURE_2D);
		Color.WHITE.render;
		auto grid = _grid.subGrid(area.min.max(Vector2i.ZERO), area.max.min(Vector2i(_grid.width, _grid.height)));
		foreach(coord, tile; grid)
		{
			if(tile.type !is null)
			{
				const height = (tile.occupants.length > 0) ? 0 : tile.type.height;

				glBindTexture(GL_TEXTURE_2D, tile.type.textures[TileFace.TOP]);
				glNormal3f(0, 0, 1);
				for(int y = 0;  y < 4;  ++y)
				{
					glBegin(GL_QUAD_STRIP);
					for(int x = 0;  x <= 4;  ++x)
					{
						glTexCoord2f(x*0.25, y*0.25); glVertex3f(coord.x + x*0.25, coord.y + y*0.25, height);
						glTexCoord2f(x*0.25, (y + 1)*0.25); glVertex3f(coord.x + x*0.25, coord.y + (y + 1)*0.25, height);
					}
					glEnd();
				}

				glBindTexture(GL_TEXTURE_2D, tile.type.textures[TileFace.SOUTH]);
				glNormal3f(0, -1, 0);
				for(int y = 0;  y < 4;  ++y)
				{
					glBegin(GL_QUAD_STRIP);
					for(int x = 0;  x <= 4;  ++x)
					{
						glTexCoord2f(x*0.25, y*0.25); glVertex3f(coord.x + x*0.25, coord.y, y*0.25*height);
						glTexCoord2f(x*0.25, (y + 1)*0.25); glVertex3f(coord.x + x*0.25, coord.y, (y + 1)*0.25*height);
					}
					glEnd();
				}

				glBindTexture(GL_TEXTURE_2D, tile.type.textures[TileFace.EAST]);
				glNormal3f(1, 0, 0);
				for(int y = 0;  y < 4;  ++y)
				{
					glBegin(GL_QUAD_STRIP);
					for(int x = 0;  x <= 4;  ++x)
					{
						glTexCoord2f(x*0.25, y*0.25); glVertex3f(coord.x + 1, coord.y + 1 - x*0.25, y*0.25*height);
						glTexCoord2f(x*0.25, (y + 1)*0.25); glVertex3f(coord.x + 1, coord.y + 1 - x*0.25, (y + 1)*0.25*height);
					}
					glEnd();
				}

				glBindTexture(GL_TEXTURE_2D, tile.type.textures[TileFace.NORTH]);
				glNormal3f(0, 1, 0);
				for(int y = 0;  y < 4;  ++y)
				{
					glBegin(GL_QUAD_STRIP);
					for(int x = 0;  x <= 4;  ++x)
					{
						glTexCoord2f(x*0.25, y*0.25); glVertex3f(coord.x + 1 - x*0.25, coord.y + 1, y*0.25*height);
						glTexCoord2f(x*0.25, (y + 1)*0.25); glVertex3f(coord.x + 1 - x*0.25, coord.y + 1, (y + 1)*0.25*height);
					}
					glEnd();
				}

				glBindTexture(GL_TEXTURE_2D, tile.type.textures[TileFace.WEST]);
				glNormal3f(-1, 0, 0);
				for(int y = 0;  y < 4;  ++y)
				{
					glBegin(GL_QUAD_STRIP);
					for(int x = 0;  x <= 4;  ++x)
					{
						glTexCoord2f(x*0.25, y*0.25); glVertex3f(coord.x, coord.y + x*0.25, y*0.25*height);
						glTexCoord2f(x*0.25, (y + 1)*0.25); glVertex3f(coord.x, coord.y + x*0.25, (y + 1)*0.25*height);
					}
					glEnd();
				}
			}
			foreach(obj; tile.occupants.keys) objs[obj.zIndex][obj] = true;
		}
		glDisable(GL_TEXTURE_2D);

		foreach(layer; objs) foreach(obj; layer.keys) obj.render;

		foreach(effect; _effects.keys)
		{
			effect.render;
			_effects.remove(effect);
		}
	}

	Map.Tile getTile(const Vector2i coord)
	{
		return _grid[coord.x, coord.y];
	}
	bool isShootable(const Vector2i coord)
	{
		return _grid[coord.x, coord.y].type.passable;
	}
	bool isPassable(const Vector2i coord)
	{
		if(!_grid[coord.x, coord.y].type.passable) return false;
		foreach(occupant; _grid[coord.x, coord.y].occupants.keys) if(!occupant.passable) return false;
		return true;
	}
	bool isEmpty(const Vector2i coord)
	{
		return _grid[coord.x, coord.y].type.passable  &&  _grid[coord.x, coord.y].occupants.length == 0;
	}

	TileType getTileType(const Vector2i coord)
	{
		return _grid[coord.x, coord.y].type;
	}
	void setTileType(const Vector2i coord, TileType type)
	{
		if(coord.x >= 0  &&  coord.y >= 0  &&  coord.x < _grid.width  &&  coord.y < _grid.height)
			_grid.get(coord.x, coord.y).type = type;
	}

	void enterTile(const Vector2i coord, MapObject mobj)
	{
		_grid.get(coord.x, coord.y).occupants[mobj] = true;
		mobj.coord = coord;
	}
	void leaveTile(const Vector2i coord, MapObject mobj)
	{
		_grid.get(coord.x, coord.y).occupants.remove(mobj);
	}
	MapObject[] getTileOccupants(const Vector2i coord)
	{
		return _grid[coord.x, coord.y].occupants.keys;
	}
	void clearTileOccupants(const Vector2i coord)
	{
		foreach(k; _grid[coord.x, coord.y].occupants.keys) _grid[coord.x, coord.y].occupants.remove(k);
	}

	Region2 usedRegion()
	{
		auto result = Region2(width, height, 0, 0);
		foreach(coord, tile; _grid)
		{
			if(tile.type !is _fill)
			{
				result = result.merge(coord);
			}
		}

		return result;
	}

	void addIterator(IIterator iterator)
	{
		_iterators[iterator] = true;
	}
	void removeIterator(IIterator iterator)
	{
		_iterators.remove(iterator);
	}
	void iterate()
	{
		foreach(iterator; _iterators.keys)
		{
			iterator.iterate();
		}
	}

	void addEffect(IEffect effect)
	{
		_effects[effect] = true;
	}

	bool trace(Vector2 from, Vector2 to)
	{
		const delta = (to - from).normal;
		const ifrom = Vector2i(cast(int)from.x, cast(int)from.y);
		const ito = Vector2i(cast(int)to.x, cast(int)to.y);

		if(ifrom.x != ito.x)
		{
			const deltaY = delta.y/delta.x;
			if(delta.x > 0)
			{
				real y = from.y + (ifrom.x + 1 - from.x)*deltaY;
				for(int x = ifrom.x + 1;  x <= ito.x;  ++x)
				{
					if(!isShootable(Vector2i(x, cast(int)y))) return false;
					y += deltaY;
				}
			}
			else
			{
				real y = from.y - (from.x - ifrom.x)*deltaY;
				for(int x = ifrom.x - 1;  x >= ito.x;  --x)
				{
					if(!isShootable(Vector2i(x, cast(int)y))) return false;
					y -= deltaY;
				}
			}
		}

		if(ifrom.y != ito.y)
		{
			const deltaX = delta.x/delta.y;
			if(delta.y > 0)
			{
				real x = from.x + (ifrom.y + 1 - from.y)*deltaX;
				for(int y = ifrom.y + 1;  y <= ito.y;  ++y)
				{
					if(!isShootable(Vector2i(cast(int)x, y))) return false;
					x += deltaX;
				}
			}
			else
			{
				real x = from.x - (from.y - ifrom.y)*deltaX;
				for(int y = ifrom.y - 1;  y >= ito.y;  --y)
				{
					if(!isShootable(Vector2i(cast(int)x, y))) return false;
					x -= deltaX;
				}
			}
		}

		return true;
	}

	uint width() { return _grid.width; }
	uint height() { return _grid.height; }

	Game game() { return _game; }

	int depth() { return _depth; }

	Tileset tileset() { return _tileset; }

	protected {
		Game				_game;
		DynamicGrid!(Tile)	_grid;
		int					_depth;
		TileType			_fill;
		Tileset				_tileset;
		bool[IIterator]		_iterators;
		bool[IEffect]		_effects;
	}
}
